﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Celeriq.Utilities;
using System.IO;
using Celeriq.Common;
using System.Collections;
using System.ServiceModel;
using System.Runtime.Serialization;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Reflection;
using System.Security.Permissions;
using System.Security;
using Celeriq.RepositoryAPI;

namespace Celeriq.Graphing.Core
{
	[Serializable()]
	[KnownType(typeof(RepositoryDefinition))]
	[KnownType(typeof(FieldDefinition))]
	[KnownType(typeof(DimensionDefinition))]
	[KnownType(typeof(IFieldDefinition))]
	[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
	public class SystemCore : MarshalByRefObject, Celeriq.Graphing.Core.ISystemCore, IDisposable
	{
		#region Class Members

		private List<RemotingObjectCache> _repositoryActives = new List<RemotingObjectCache>();
		private ConfigurationSettings _configuration = null;
		private System.Timers.Timer _timerDelayLoad = null;
		private System.Timers.Timer _timer = null;
		private ServerResourceSettings _resourceSettings = null;
		private KeyPair _masterKeys = null;

		#endregion

		#region Constructors

		public SystemCore()
			: base()
		{
			InitKeys();

			_timerDelayLoad = new System.Timers.Timer(2000);
			_timerDelayLoad.Elapsed += DelayStartup;
			_timerDelayLoad.Start();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="restart">Determines if previously loaded repositories should be restarted automatically</param>
		public SystemCore(bool restart)
			: this()
		{
			if (restart)
				RestartRepositories();
		}

		private void DelayStartup(object sender, System.Timers.ElapsedEventArgs e)
		{
			_timerDelayLoad.Stop();
			_timerDelayLoad = null;

			lock (_repositoryActives)
			{
				#region HouseKeeping

				//Create key file if need be
				var keyFile = ConfigurationSettings.GetKeyFile();
				if (File.Exists(keyFile))
				{
					_masterKeys = SecurityHelper.DecryptObjectFromDisk<KeyPair>(keyFile);
				}
				else
				{
					_masterKeys = SecurityHelper.GenerateSymmetricKeys();
					SecurityHelper.EncryptObjectToDisk<KeyPair>(keyFile, _masterKeys);
				}

				//Initialize user file if need be
				InitUserFile();

				#endregion

				_configuration = ConfigurationSettings.GetInstance();
				_resourceSettings = new ServerResourceSettings();
				_resourceSettings = _resourceSettings.Load(Path.Combine(_configuration.InstallPath, "celeriq.configuration"));

				//Verify that all permissions are in place
				if (!this.IsSetupValid())
				{
					throw new Exception("This application does not have the proper permissions!");
				}

				Logger.LogInfo("Loading Data Path: '" + _configuration.DataPath + "'");
				var files = Directory.GetFiles(_configuration.DataPath, "*.celeriq");
				foreach (var f in files)
				{
					var repository = RepositoryDefinition.FromDisk(f);
					repository.CachePath = Path.Combine(_configuration.DataPath, repository.ID.ToString());
					var remoteItem = new RemotingObjectCache() { Repository = repository, VersionHash = repository.VersionHash };
					_repositoryActives.Add(remoteItem);
					StartRepository(remoteItem.Repository.ID, this.RootUser);
				}
			}

			_timer = new System.Timers.Timer(20000);
			_timer.Elapsed += _timer_Elapsed;
			_timer.Start();
		}

		private void InitKeys()
		{
			lock (_repositoryActives)
			{
				//Create key file if need be
				var keyFile = ConfigurationSettings.GetKeyFile();
				if (File.Exists(keyFile))
				{
					_masterKeys = SecurityHelper.DecryptObjectFromDisk<KeyPair>(keyFile);
				}
				else
				{
					_masterKeys = SecurityHelper.GenerateSymmetricKeys();
					SecurityHelper.EncryptObjectToDisk<KeyPair>(keyFile, _masterKeys);
				}
			}
		}

		#endregion

		#region Event Handlers
		private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
		{
			lock (_repositoryActives)
			{
				#region This will unload the data in a repository if there is a set # of minutes to keep inactive repository
				var wasUnloaded = false;
				foreach (var item in _repositoryActives.Where(x => x.GetIsLoaded()))
				{
					if (item.ServiceInstance.LastAccess != DateTime.MinValue &&
							(_resourceSettings.AutoDataUnloadTime > 0) &&
							(DateTime.Now.Subtract(item.ServiceInstance.LastAccess).TotalMinutes >= _resourceSettings.AutoDataUnloadTime))
					{
						item.ServiceInstance.UnloadData();
						wasUnloaded = true;
					}
				}
				#endregion

				#region Unload if thre are too many repositories in memory

				if (_resourceSettings.MaxRunningRepositories > 0)
				{
					var tooManyList = _repositoryActives
						.Where(x => x.GetIsLoaded())
						.OrderByDescending(x => x.ServiceInstance.LastAccess)
						.Skip(_resourceSettings.MaxRunningRepositories)
						.ToList();

					foreach (var item in tooManyList)
					{
						item.ServiceInstance.UnloadData();
						wasUnloaded = true;
					}
				}

				#endregion

				#region Unload if used too much memory
				if (_resourceSettings.MaxMemory > 0)
				{
					long TOTAL = _resourceSettings.MaxMemory * 1024 * 1024;
					var list = _repositoryActives.Where(x => x.GetIsLoaded()).OrderByDescending(x => x.ServiceInstance.LastAccess).ToList();
					long usedMemory = 0;
					foreach (var item in list)
					{
						usedMemory += item.ServiceInstance.GetDataMemorySize(RootUser);
						if (usedMemory > TOTAL)
						{
							item.ServiceInstance.UnloadData();
							wasUnloaded = true;
						}
					}
				}
				#endregion

				//collect garbage to free memory
				if (wasUnloaded)
				{
					GC.Collect();
				}

			}
		}
		#endregion

		#region Properties

		private SystemCredentials RootUser
		{
			get
			{
				var user = UserDomain.UserList.FirstOrDefault(x => x.UserName == "root");
				if (user == null) return null;
				user = new SystemCredentials() { UserName = user.UserName, Password = user.Password };
				user.Password = Celeriq.Utilities.SecurityHelper.Encrypt(this.GetPublicKey(), user.Password);
				return user;
			}
		}
		

		public List<RemotingObjectCache> GetRepositoryPropertyList(UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				try
				{
					foreach (var item in _repositoryActives)
					{
						if (item.IsRunning)
						{
							item.ItemCount = item.ServiceInstance.GetItemCount(credentials);
							item.DataDiskSize = item.ServiceInstance.GetDataDiskSize(credentials);
							item.DataMemorySize = item.ServiceInstance.GetDataMemorySize(credentials);
						}
					}
					var list = _repositoryActives.Select(x => x).OrderBy(x => x.Repository.Name).ToList();
					list.ForEach(x => x.IsLoaded = x.GetIsLoaded());
					return list;
				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					return null;
				}
			}
		}

		#endregion

		#region Methods

		public string GetPublicKey()
		{
			return _masterKeys.PublicKey;
		}

		public bool IsValidCredentials(UserCredentials credentials)
		{
			try
			{
				var user = UserDomain.UserList.FirstOrDefault(x => x.UserName == credentials.UserName);
				if (user == null) return false;
				var prehash = Celeriq.Utilities.SecurityHelper.Decrypt(_masterKeys.PrivateKey, credentials.Password);
				return (prehash == user.Password);
			}
			catch (Exception ex)
			{
				return false;
			}
		}

		private void InitUserFile()
		{
			if (UserDomain.UserList.Count == 0)
			{
				UserDomain.AddUser(new SystemCredentials { UserName = "root", Password = "password" });
			}
		}

		private bool IsSetupValid()
		{
			var writePermission = new FileIOPermission(FileIOPermissionAccess.Write, _configuration.DataPath);
			if (!SecurityManager.IsGranted(writePermission))
			{
				Logger.LogError("The service does not have permission to access the install folder!");
				return false;
			}

			var eventLogPermission = new System.Diagnostics.EventLogPermission();
			try
			{
				eventLogPermission.Demand();
			}
			catch (Exception ex)
			{
				Logger.LogError("The service does not have permission to the event log!");
				return false;
			}
			return true;
		}

		public RemotingObjectCache SaveRepository(RepositoryDefinition repository, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				if (repository == null)
					throw new Exception("Object cannot be null!");

				Logger.LogInfo("SaveRepository: " + repository.ID);
				if (repository.CreatedDate == DateTime.MinValue)
					repository.CreatedDate = DateTime.Now;

				try
				{
					var fileName = Path.Combine(_configuration.DataPath, repository.ID.ToString() + ".celeriq");
					repository.ToDisk(fileName);

					var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == repository.ID);
					if (active == null)
					{
						active = new RemotingObjectCache() { Repository = repository, VersionHash = repository.VersionHash };
						_repositoryActives.Add(active);
						active = StartRepository(active.Repository.ID, this.RootUser);
					}
					else
					{
						//Save state
						var isOn = active.IsRunning;

						if (!active.IsRunning)
							active = StartRepository(active.Repository.ID, this.RootUser);

						active.Repository = RepositoryDefinition.FromDisk(fileName);

						//For now lets just try to stop and start the repository
						ResetRepository(repository.ID);
						active.VersionHash = repository.VersionHash;

						if (!isOn)
							active = StopRepository(active.Repository.ID, this.RootUser);
					}

					return active;
				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					return null;
				}
			}
		}

		private void UnloadOldest()
		{
			var item = _repositoryActives
				.Where(x => x.IsRunning)
				.OrderBy(x => x.ServiceInstance.LastAccess)
				.FirstOrDefault();

			if (item != null)
			{
				item.ServiceInstance.UnloadData();
			}
		}

		private int LoadedCount()
		{
			return _repositoryActives.Count(x => x.IsRunning && x.GetIsLoaded());
		}

		public bool RepositoryExists(Guid serverId, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				return _repositoryActives.Any(x => x.Repository.ID == serverId);
			}
		}

		public RemotingObjectCache DataLoadRepository(Guid serverId, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == serverId && x.IsRunning);
				if (active == null) return null; //cannot find repository, should never happen
				if (active.ServiceInstance == null) return null; //cannot find repository, should never happen
				if (active.GetIsLoaded()) return active;
					active.ServiceInstance.LoadData(credentials);
				return active;
			}
		}

		public RemotingObjectCache StartRepository(Guid serverId, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				//If there is a maximum # of repository defined then stop least used if necessary
				if ((_resourceSettings.MaxRunningRepositories > 0) && LoadedCount() >= _resourceSettings.MaxRunningRepositories)
					UnloadOldest();

				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == serverId);
				if (active == null) return null; //cannot find repository, should never happen
				if (active.Host != null) return active; //if already has a host object then it is active, do not restart

				Logger.LogInfo("Starting Repository: " + serverId);
				try
				{
					//Start the service
					Logger.LogInfo("Server '" + serverId.ToString() + "' is Loading...");

					//Load Server Object
					var baseAddress = new Uri("net.tcp://localhost:1973/" + active.Repository.ID.ToString());
					var serviceInstance = new Celeriq.RepositoryAPI.RepositoryService();

					//Load data into this service object
					var startUp = new Celeriq.Utilities.ServiceStartup()
					{
						RepositoryFolder = _configuration.DataPath,
						RepositoryDefinition = active.Repository,
						CacheLength = 300,
					};
					var count = serviceInstance.StartUp(startUp);
					var host = new ServiceHost(serviceInstance, baseAddress);

					//Initialize the service
					var myBinding = new NetTcpBinding()
					{
						MaxBufferSize = int.MaxValue,
						MaxReceivedMessageSize = int.MaxValue,
						MaxBufferPoolSize = int.MaxValue,
						ReaderQuotas = new System.Xml.XmlDictionaryReaderQuotas()
						{
							MaxArrayLength = int.MaxValue,
							MaxBytesPerRead = int.MaxValue,
							MaxDepth = int.MaxValue,
							MaxNameTableCharCount = int.MaxValue,
							MaxStringContentLength = int.MaxValue,
						}
					};
					myBinding.Security.Mode = SecurityMode.None;
					var endpoint = host.AddServiceEndpoint(typeof(Celeriq.Common.IDataModel), myBinding, host.BaseAddresses.First().AbsoluteUri);

					foreach (var op in endpoint.Contract.Operations)
					{
						var dataContractBehavior = op.Behaviors.Find<DataContractSerializerOperationBehavior>();
						if (dataContractBehavior != null)
						{
							dataContractBehavior.MaxItemsInObjectGraph = int.MaxValue;
						}
					}

					//var behavior = host.Description.Behaviors.Find<ServiceDebugBehavior>();
					//behavior.IncludeExceptionDetailInFaults = true;

					host.Open();
					active.Host = host;
					active.ItemCount = count;
					active.ServiceInstance = serviceInstance as IRepositoryService;
					active.DataDiskSize = active.ServiceInstance.GetDataDiskSize(credentials);
					active.DataMemorySize = active.ServiceInstance.GetDataMemorySize(credentials);
					active.IsRunning = true;

					Logger.LogInfo("Server '" + serverId.ToString() + "' is Loaded (" + count + " items)");

					return active;
				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					throw;
				}
			}

		}

		public RemotingObjectCache StopRepository(Guid serverId, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				try
				{
					var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == serverId);
					if (active == null) return null; //cannot find repository, should never happen
					if (active.Host == null) return active; //if already stopped do not perform again

					if (active.Host != null)
					{
						Logger.LogInfo("Stopping Repository: " + serverId);
						Logger.LogInfo("Server '" + serverId.ToString() + "' is Unloading...");
						active.ServiceInstance.ShutDown();
						active.Host.Close();
						active.Host = null;
						active.ServiceInstance = null;
						active.IsRunning = false;
						Logger.LogInfo("Server '" + serverId.ToString() + "' is Unloaded");
					}
					return active;

				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					throw;
				}
			}

		}

		public void DeleteRepository(RepositoryDefinition repository, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == repository.ID);
				if (active != null)
				{
					StopRepository(repository.ID, credentials);

					var dataPath = Path.Combine(_configuration.DataPath, repository.ID.ToString());
					if (Directory.Exists(dataPath))
						Directory.Delete(dataPath, true);
					var fileName = Path.Combine(_configuration.DataPath, repository.ID.ToString() + ".celeriq");
					if (File.Exists(fileName))
						File.Delete(fileName);

					_repositoryActives.Remove(active);
				}
			}
		}

		public void Compress(Guid serverId, UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == serverId);
				if (active == null) return; //cannot find repository, should never happen

				Logger.LogInfo("Compressing Repository: " + serverId.ToString());
				try
				{
					active.ServiceInstance.Compress(credentials);
				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					throw;
				}

				Logger.LogInfo("Compressed Repository: " + serverId.ToString());
			}
		}

		/// <summary>
		/// Deletes all data from a repository and starts fresh
		/// </summary>
		/// <param name="serverId"></param>
		/// <returns></returns>
		private bool ResetRepository(Guid serverId)
		{
			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == serverId);
				if (active == null) return false; //cannot find repository, should never happen

				Logger.LogInfo("Resetting Repository: " + serverId.ToString());
				try
				{
					var startUp = new Celeriq.Utilities.ServiceStartup()
					{
						RepositoryFolder = _configuration.DataPath,
						RepositoryDefinition = active.Repository,
						CacheLength = 300,
					};

					if (active.ServiceInstance == null)
					{
						var serviceInstance = new Celeriq.RepositoryAPI.RepositoryService();
						serviceInstance.Reset(startUp);
					}
					else
					{
						active.ServiceInstance.Reset(startUp);
					}

				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					throw;
				}

				Logger.LogInfo("Reset Repository: " + serverId.ToString());
				return true;
			}
		}

		/// <summary>
		/// Shuts down all repositories
		/// </summary>
		public void ShutDown()
		{
			lock (_repositoryActives)
			{
				var stateConfig = new RepositoryStateConfiguration();
				stateConfig.RunningRepositories.Clear();

				foreach (var item in _repositoryActives)
				{
					if (item.IsRunning) stateConfig.RunningRepositories.Add(item.Repository.ID);
					this.StopRepository(item.Repository.ID, this.RootUser);
				}
				stateConfig.Save();

				if (_resourceSettings != null && _configuration != null)
					_resourceSettings.Save(Path.Combine(_configuration.InstallPath, "celeriq.configuration"));

				Logger.LogInfo("System.Core ShutDown");
			}
		}

		/// <summary>
		/// Restart all repositories that were loaded on the last shutdown
		/// </summary>
		public void RestartRepositories()
		{
			lock (_repositoryActives)
			{
				try
				{
					//Reload Running Repositories
					var stateConfig = new RepositoryStateConfiguration();
					foreach (var g in stateConfig.RunningRepositories)
					{
						var item = _repositoryActives.FirstOrDefault(x => x.Repository.ID == g);
						if (item != null) StartRepository(g, this.RootUser);
					}
				}
				catch (Exception ex)
				{
					Logger.LogError(ex.ToString());
					throw;
				}
			}
		}

		public bool ExportSchema(Guid repositoryId, UserCredentials credentials, string backupFile)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == repositoryId);
				if (active == null)
				{
					return false;
				}

				var startUp = new Celeriq.Utilities.ServiceStartup()
				{
					RepositoryFolder = _configuration.DataPath,
					RepositoryDefinition = active.Repository,
					CacheLength = 300,
				};

				var instance = active.ServiceInstance;
				if (instance == null)
					instance = new Celeriq.RepositoryAPI.RepositoryService();

				instance.ExportSchema(startUp, credentials, backupFile);
				return true;
			}
		}

		public bool Backup(Guid repositoryId, UserCredentials credentials, string backupFile)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == repositoryId);
				if (active == null)
				{
					return false;
				}

				var startUp = new Celeriq.Utilities.ServiceStartup()
				{
					RepositoryFolder = _configuration.DataPath,
					RepositoryDefinition = active.Repository,
					CacheLength = 300,
				};

				var instance = active.ServiceInstance;
				if (instance == null)
					instance = new Celeriq.RepositoryAPI.RepositoryService();

				instance.Backup(startUp, credentials, backupFile);
				return true;
			}
		}

		public bool Restore(Guid repositoryId, UserCredentials credentials, string backupFile)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			lock (_repositoryActives)
			{
				var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == repositoryId);
				if (active == null)
				{
					return false;
				}

				var startUp = new Celeriq.Utilities.ServiceStartup()
				{
					RepositoryFolder = _configuration.DataPath,
					RepositoryDefinition = active.Repository,
					CacheLength = 300,
				};

				var instance = active.ServiceInstance;
				if (instance == null)
					instance = new Celeriq.RepositoryAPI.RepositoryService();

				instance.Restore(startUp, credentials, backupFile);

				return true;
			}
		}

		public string[] GetBackups(UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			var retval = new List<string>();
			var backupFolder = Path.Combine(_configuration.DataPath, @"..\Backup");
			if (Directory.Exists(backupFolder))
			{
				var files = Directory.GetFiles(backupFolder);
				foreach (var file in files)
				{
					var fi = new FileInfo(file);
					retval.Add(fi.Name);
				}
			}
			return retval.ToArray();
		}

		public ServerResourceSettings GetServerResourceSetting(UserCredentials credentials)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			return _resourceSettings;
		}

		public bool SaveServerResourceSetting(UserCredentials credentials, ServerResourceSettings settings)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");
			
			_resourceSettings = settings;
			_resourceSettings.Save(Path.Combine(_configuration.InstallPath, "celeriq.configuration"));
			return true;
		}

		public ProfileItem[] GetProfile(Guid repositoryId, UserCredentials credentials, long lastProfileId)
		{
			if (!IsValidCredentials(credentials))
				throw new Exception("Invalid credentials");

			var active = _repositoryActives.FirstOrDefault(x => x.Repository.ID == repositoryId);
			if (active == null)
			{
				return new ProfileItem[] { };
			}
			else if (!active.IsLoaded)
			{
				return new ProfileItem[] { };
			}
			return active.ServiceInstance.GetProfile(credentials, lastProfileId);
		}

		#endregion

		void IDisposable.Dispose()
		{
			this.ShutDown();
		}

	}
}